/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved.
 *
 */

import { createHash, Hash } from 'crypto';
import fs from 'fs';
import fsp from 'fs/promises';
import { createReadStream } from 'node:fs';
import path from 'path';
import { pipeline } from 'stream/promises';
import { hvigorCore, maxPathLength } from '@ohos/hvigor';
import { getAlignTarget } from '@ohos/hvigor/src/base/util/task-util';
import fse from 'fs-extra';
import { glob } from 'glob';
import { CommonConst, ParameterizationConst } from '../const/common-const.js';
import { PackingOptAsset } from '../options/build/build-opt.js';
import { matchValueWithParameterizationKey, OhPackageInfoGetter, ParameterizationMatchResult } from '../validate/dependenct-param-validate.js';
import { pathParamsOpt } from '../validate/dependency-tag-validate.js';
import { getJson5Obj } from './json-util.js';
import { ohPackageLoader } from './loader/file/oh-package-loader.js';
import { OhosLogger } from './log/ohos-logger.js';

/**
 * 处理文件的工具类
 *
 * @since 2021/12/29
 */
export class FileUtil {
  private static _log: OhosLogger = OhosLogger.getLogger('File');

  static async linkFile(source: string, destination: string, followLink = false): Promise<void> {
    const stats = await fsp.lstat(source);
    if (stats.isDirectory()) {
      return;
    }
    const realPath = followLink && stats.isSymbolicLink() ? await fsp.readlink(source) : source;
    if (!fs.existsSync(realPath)) {
      return;
    }
    await fse.ensureDir(path.dirname(destination));
    try {
      // 测试是否有访问权限
      await fsp.access(realPath, fs.constants.R_OK);
      await fsp.link(source, destination);
      this._log.debug(`Link ${realPath} to ${destination} successfully.`);
    } catch (e) {
      this._log.debug(`Hard link from ${realPath} to ${destination} failed. Do copy instead.`);
      await fsp.copyFile(realPath, destination);
    }
  }

  static isDirectory(...pathSegments: string[]): boolean {
    return fse.statSync(path.resolve(...pathSegments)).isDirectory();
  }

  static checkDirWithoutDelete(...pathSegments: string[]): void {
    const dirPath = path.resolve(...pathSegments);
    if (!fse.existsSync(dirPath)) {
      fse.mkdirSync(dirPath, { recursive: true });
    }
  }

  static checkFile(...pathSegments: string[]): void {
    const filePath = path.resolve(...pathSegments);
    if (!fse.existsSync(filePath)) {
      FileUtil.makeFile(filePath);
    }
    fse.removeSync(filePath);
    FileUtil.makeFile(filePath);
  }

  static makeFile(filePath: string, data = ''): void {
    fse.writeFileSync(filePath, data);
  }

  static deleteFile(filePath: string): void {
    try {
      if (fse.existsSync(filePath)) {
        fse.removeSync(filePath);
      }
    } catch (e) {
      this._log.printErrorExit('DELETE_FILE_ERROR', [filePath]);
    }
  }

  static readFile(filePath: string): string | undefined {
    try {
      if (!fse.existsSync(filePath)) {
        return undefined;
      }
      return fse.readFileSync(filePath, { encoding: 'utf-8' });
    } catch (e) {
      this._log.error(`Please check file at ${filePath}.`);
      return undefined;
    }
  }

  /**
   * 校验路径长度
   *
   * @param path 待校验的路径
   */
  static checkPathLength(path: string): void {
    const maxLength: number = maxPathLength();
    if (path.length > maxLength) {
      this._log.printErrorExit('FILE_PATH_EXCEED_LIMITATION', [`${  maxLength}`]);
    }
  }

  static async hashEntry(dir: string, pattern = '**/*') {
    const result = new Map<string, string>();
    const files = glob.sync(pattern, { nodir: true, absolute: true, cwd: dir, follow: false });
    for (const file of files) {
      const hash = await this.pipelineAsync(file);
      result.set(file, hash);
    }
    return result;
  }

  private static async pipelineAsync(fullPath: string): Promise<string> {
    if (!hvigorCore?.getParameter()?.getProperty(CommonConst.OHOS_PROCESSLIB_OPTIMIZATION)) {
      this._log.debug(`Check content hash for ${fullPath}`);
      return await this.hashContent(fullPath);
    }
    this._log.debug(`Check size & timestamp for ${fullPath}`);
    return this.hashStats(fullPath);
  }

  static async hashContent(file: string): Promise<string> {
    if (!fse.existsSync(file)) {
      throw new Error(`File "${  file  }" does not exist`);
    }
    const hash: Hash = createHash('md5');
    await pipeline(createReadStream(file), hash);
    return hash.digest('hex');
  }

  static hashStats(file: string): string {
    if (!fse.existsSync(file)) {
      throw new Error(`File "${  file  }" does not exist`);
    }
    const hash: Hash = createHash('md5');
    const stats = fs.statSync(file);
    hash.update(stats.size.toString() + stats.mtimeMs);
    return hash.digest('hex');
  }

  /**
   * 将路径转换为绝对路径
   *
   * @param {string} dir 文件或目录路径
   * @param {string} currentDir 当前路径，参考路径
   * @return {string} 转换后的绝对路径
   */
  static convertToAbsolutePath(dir: string, currentDir: string): string {
    return path.isAbsolute(dir) ? dir : path.resolve(currentDir, dir);
  }

  static convertToAbsolutePaths(dirs: string[], currentDir: string): string[] {
    return dirs.map((dir) => FileUtil.convertToAbsolutePath(dir, currentDir));
  }

  static isSubDir(parent: string, child: string) {
    const relative = path.relative(parent, child);
    return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
  }

  /**
   * 校验文件是否存在
   *
   * @param completePath 文件完整路径
   */
  static fileExists(completePath: string) {
    return fs.existsSync(completePath) && fs.realpathSync.native(completePath) === completePath;
  }

  /**
   * 匹配文件或目录是否存在
   *
   * @param filePath 文件或目录路径
   * @param nocase 是否忽略大小写，true: yes, false: no
   */
  static matchingFile(filePath: string, nocase = false) {
    const arr: string[] = glob.sync(filePath, { nocase });
    return !!arr.length;
  }

  // 对特殊文件进行判断
  static copySpecialFileToTempDir(_moduleDir: string, taskTmpDir: string) {
    fse.readdirSync(_moduleDir).forEach((fileName) => {
      if (this.checkSpecialFile(fileName)) {
        const fileSrc = path.resolve(_moduleDir, fileName);

        // 判断是否为文件
        if (fse.statSync(fileSrc).isFile()) {
          fse.copySync(fileSrc, path.resolve(taskTmpDir, fileName));
        }
      }
    });
  }

  /**
   * 处理打包配置
   * @param _moduleDir 模块根目录
   * @param asset 配置项内容
   * @param taskTmpDir temp文件夹
   */
  static disposalPackingOptionAsset(_moduleDir: string, asset: PackingOptAsset | undefined, taskTmpDir: string) {
    if (!asset?.include?.length && !asset?.exclude?.length) {
      return;
    }
    this.disposalPackingOptInclude(asset.include, _moduleDir, taskTmpDir);
    this.disposalPackingOptExclude(asset.exclude, taskTmpDir);
  }

  /**
   * 处理打包配置中的白名单配置
   * @param includes 白名单
   * @param _moduleDir 模块根目录
   * @param taskTmpDir temp文件夹
   */
  private static disposalPackingOptInclude(includes: string[] | undefined, _moduleDir: string, taskTmpDir: string) {
    if (!includes?.length) {
      return;
    }
    const IGNORE_DIR: string[] = ['node_modules/**', 'oh_modules/**', '.preview/**', 'build/**', '.cxx/**', '.test/**'];
    // 不在当前模块下的文件也过滤掉
    const MODULE_DIR = path.resolve(_moduleDir);
    const fileNames: Set<string> = new Set<string>();
    // 遍历配置项
    includes.forEach((pattern) => {
      const files: string[] = glob.sync(pattern, { cwd: _moduleDir, ignore: IGNORE_DIR });
      if (!files?.length) {
        return;
      }
      // 去重
      files.forEach((fileName: string) => {
        fileNames.add(fileName);
      });
    });
    // 将文件复制到缓存中
    fileNames.forEach((filePath) => {
      const fileSrc = path.resolve(_moduleDir, filePath);
      // 是否在模块下，路径是否真实存在
      if (fileSrc.startsWith(MODULE_DIR) && fse.existsSync(fileSrc)) {
        fse.copySync(fileSrc, path.resolve(taskTmpDir, filePath));
      }
    });
  }

  /**
   * 处理打包配置中的黑名单配置
   * @param excludes 黑名单
   * @param taskTmpDir temp文件夹
   */
  private static disposalPackingOptExclude(excludes: string[] | undefined, taskTmpDir: string) {
    if (!excludes?.length) {
      return;
    }
    const IGNORE_DIR: string[] = ['oh-package.json5', 'ResourceTable.txt', 'Index.d.ets', 'ets/**', 'src/main/module.json'];
    // 不在当前目录下的文件也过滤掉
    const TASK_TMP_DIR = path.resolve(taskTmpDir);
    // 遍历配置项
    excludes.forEach((pattern) => {
      const files: string[] = glob.sync(pattern, { cwd: taskTmpDir, ignore: IGNORE_DIR });
      if (!files?.length) {
        return;
      }
      // 删除文件
      files.forEach((filePath: string) => {
        const fileSrc = path.resolve(taskTmpDir, filePath);
        if (fileSrc.startsWith(TASK_TMP_DIR) && fse.existsSync(fileSrc)) {
          fse.removeSync(fileSrc);
        }
      });
    });
  }

  /**
   * 递归读取一个文件夹下面的所有文件名
   * @param dir
   */
  static async readDirFileNames(dir: string): Promise<Set<string>> {
    const fileNames = new Set<string>();

    async function readFiles(dir: string) {
      try {
        const entries = await fse.readdir(dir, { withFileTypes: true });
        for (const entry of entries) {
          const fullPath = path.join(dir, entry.name);
          if (entry.isDirectory()) {
            await readFiles(fullPath);
          } else {
            fileNames.add(entry.name);
          }
        }
      } catch (e: any) {
        // no op
      }
    }

    await readFiles(dir);
    return fileNames;
  }

  /**
   * 在不改变原有package.json的情况下添加参数useNormalizedOHMUrl到临时的package.json文件中
   *
   * @param isOhpmProject
   * @param taskTmpDir
   * @param params
   * @param extraParams
   * @param targetName
   * @param libsFileNameSet libs下面的文件名集合
   */
  static addParamToOhPack(
    isOhpmProject: boolean,
    taskTmpDir: string,
    params: object,
    extraParams: ExtraParamsForHar,
    targetName: string,
    libsFileNameSet: Set<string>,
  ) {
    const packJson = isOhpmProject ? CommonConst.OH_PACKAGE_JSON5 : CommonConst.PACKAGE_JSON;
    const packJsonPath = ohPackageLoader.getNodeOhPackagePath(path.resolve(taskTmpDir, packJson));
    let packJsonObj = getJson5Obj(packJsonPath);
    if (packJsonObj.metadata) {
      Object.assign(packJsonObj.metadata, params);
    } else {
      Object.assign(packJsonObj, { metadata: params });
    }
    packJsonObj = FileUtil.mergeExtraParamsToOhPack(packJsonObj, extraParams, targetName, libsFileNameSet);
    fse.writeJSONSync(packJsonPath, packJsonObj);
  }

  static mergeExtraParamsToOhPack(packJsonObj: any, extraParams: ExtraParamsForHar, targetName: string, libsFileNameSet: Set<string>) {
    packJsonObj = {
      ...packJsonObj,
      ...extraParams,
      nativeComponents: (packJsonObj.nativeComponents || []).concat(extraParams.nativeComponents || []),
    };
    if (!extraParams.nativeComponents) {
      delete packJsonObj.nativeComponents;
    }

    if (packJsonObj.nativeComponents) {
      // 仅将收集到libs中的.a及.so放到nativeComponents中
      packJsonObj.nativeComponents = packJsonObj.nativeComponents.filter((item: NativeComponents) => libsFileNameSet.has(item.name));
    }

    // 开关打开时需要在version后加入对应的targetName
    if (targetName === getAlignTarget()) {
      packJsonObj.version += `+${getAlignTarget()}`;
    }

    return packJsonObj;
  }

  /**
   * 编译构建shared library模块的har产物中添加额外的文件，不区分大小写
   * readme.opensource、license、changelog.md、readme.md及changelog与readme的国际化文件
   * @param fileName string
   */
  static checkSpecialFile(fileName: string) {
    const harDescriptionFileList: string[] = ['readme.opensource'];
    const fileFirstNameList = ['license'];
    // changelog、readme的md文件匹配国际化后缀
    const fileNameReg = /^(changelog|readme)([-_](cn|en))?\.md$/i;
    const fileNameLower = fileName.toLowerCase();
    const fileFirstName = fileNameLower.split('.')[0];
    return harDescriptionFileList.indexOf(fileNameLower) !== -1 || fileFirstNameList.indexOf(fileFirstName) !== -1 || fileNameReg.test(fileNameLower);
  }

  /**
   * 重复文件名处理
   * @param fileNames  工程目录结构中读取的文件名数组
   * @param profileName 新增文件名
   * @private
   */
  static uniqueFileName(fileNames: string[], profileName: string): string | undefined {
    const fileCountMap = new Map<string, number>();
    fileNames.forEach((fileName) => {
      // 工程目录结构中读取的文件不会存在重复fileName,直接set
      fileCountMap.set(fileName, 1);
    });

    if (!fileCountMap.has(profileName)) {
      fileCountMap.set(profileName, 1);
      return profileName;
    } else {
      let count = fileCountMap.get(profileName)!;
      while (fileCountMap.has(this.addSuffix(profileName, count))) {
        count++;
      }
      fileCountMap.set(profileName, count + 1);
      const newProfileName = this.addSuffix(profileName, count);
      fileCountMap.set(newProfileName, 1);
      return newProfileName;
    }
  }

  /**
   * 给重复文件添加后缀以区分唯一文件名
   * @param name
   * @param index
   */
  static addSuffix(name: string, index: number): string {
    return `${name}${index}`;
  }

  /**
   * 遍历文件夹获取其中所有文件的文件名
   * @param fileFolderPath
   */
  static traverseFileFolder(fileFolderPath: string): string[] {
    const fileNames: string[] = [];
    if (!fse.statSync(fileFolderPath).isDirectory() || !fse.existsSync(fileFolderPath)) {
      return [];
    }

    const files = fse.readdirSync(fileFolderPath);
    files.forEach((fileName) => {
      const filePath = path.join(fileFolderPath, fileName);
      const fileStat = fse.statSync(filePath);
      if (fileStat.isDirectory()) {
        this.traverseFileFolder(filePath);
      } else if (fileStat.isFile()) {
        fileNames.push(fileName.split('.')[0]);
      }
    });
    return fileNames;
  }

  /**
   * 遍历文件夹并获取其中所有的文件路径
   * @param fileFolderPath
   */
  static getAllFilesFromFolder(fileFolderPath: string): string[] {
    const filePaths: string[] = [];
    if (!fse.existsSync(fileFolderPath) || !fse.statSync(fileFolderPath).isDirectory()) {
      return [];
    }

    const files = fse.readdirSync(fileFolderPath);
    files.forEach((fileName) => {
      const filePath = path.join(fileFolderPath, fileName);
      const fileStat = fse.statSync(filePath);
      if (fileStat.isDirectory()) {
        filePaths.push(...this.getAllFilesFromFolder(filePath));
      } else if (fileStat.isFile()) {
        filePaths.push(filePath);
      }
    });
    return filePaths;
  }

  /**
   * 获取文件格式后缀
   * @param filePath
   */
  static getFileSuffix(filePath: string): string {
    const fileName = path.parse(filePath).base;
    if (fileName === '' || fileName.indexOf('.') === -1) {
      return fileName;
    }

    return fileName.substring(fileName.lastIndexOf('.') + 1);
  }

  /**
   * 读取路径时将分隔符转为正斜杆
   * eg: a\\b\\c -> a/b/c
   *
   * @param {string} path 原始字符串
   * @returns {string} 转化后的字符串
   */
  static normalizePathSeparator(path: string): string {
    return path.replace(/\\\\/g, '/').replace(/\\/g, '/').replace(/\/:/g, ':');
  }

  /**
   * 解析jsonPathObj中使用的引用,在parameterFileObj中找到对应的value值,直接修改jsonPathObj的内容
   *
   * @param jsonPathObj  需要被替换的json对象
   * @param parameterFileObj  param对象
   * @param whiteList 白名单，不配置默认替换全部
   * @param pathParamsObj  路径参数对象，其中包括ohPackage.json5和parameter.json两个文件所在路径
   * @param ohPackageInfoGetter
   * @return string[] 如果参数化的是本地产物依赖，需要记录它的绝对路径，在打包har时把本地产物依赖打进去
   */
  static resolveJsonObjWithoutParam(
    jsonPathObj: any,
    parameterFileObj: object | undefined,
    pathParamsObj: pathParamsOpt,
    whiteList?: string[],
    ohPackageInfoGetter?: OhPackageInfoGetter,
  ): string[] {
    const localProductDependencies: string[] = [];
    for (const key in jsonPathObj) {
      // 如果配置了白名单且遍历的字段不在白名单范围内不做处理
      if (whiteList && !whiteList.includes(key)) {
        continue;
      }

      // 如果属性值为一个对象，再次调用parsePackageJsonObj进行递归
      if (typeof jsonPathObj[key] === 'object' && jsonPathObj[key] !== null) {
        const childObjLocalDepArr = this.resolveJsonObjWithoutParam(jsonPathObj[key], parameterFileObj, pathParamsObj, undefined, ohPackageInfoGetter);
        localProductDependencies.push(...childObjLocalDepArr);
      } else if (typeof jsonPathObj[key] === 'string') {
        const matchResult = FileUtil.extractValue(jsonPathObj[key], parameterFileObj, pathParamsObj, ohPackageInfoGetter, key);
        jsonPathObj[key] = matchResult.value;
        if (matchResult.localDepArr) {
          localProductDependencies.push(...matchResult.localDepArr);
        }
      }
    }
    this._log.debug(`jsonObjWithoutParam ${JSON.stringify(jsonPathObj)} at ${pathParamsObj.parameterFilePath}`);
    return localProductDependencies;
  }

  private static extractValue(
    value: string,
    parameterFileObj: object | undefined,
    pathParamsObj: pathParamsOpt,
    ohPackageInfoGetter?: OhPackageInfoGetter,
    dependencyName?: string,
  ): ParameterizationMatchResult {
    const match = value.match(ParameterizationConst.REGEX);
    if (!match) {
      return { value };
    }

    // parameterFile配置内容为空或配置文件不存在
    if (!parameterFileObj) {
      this._log.printErrorExit('PARAMETER_FILE_KEY_ERROR', [value, pathParamsObj.parameterFilePath]);
    }
    const keys = match[1].split('.');
    return matchValueWithParameterizationKey({
      keys,
      parameterFileJson: parameterFileObj as Record<string, unknown>, // 执行到此处 parameterFileObj 不可能为空
      pathParamsObj,
      dependencyName,
      ohPackageInfoGetter,
    });
  }

  /**
   * 针对oh-package.json5的依赖进行param的替换
   *
   * @param {string} value 待替换的值
   * @param dependencyName  oh-package中需要被替換的key值
   * @param {object | undefined} parameterFileObj 替换映射表对象
   * @param pathParamsObj
   * @returns {string} 替换后的真实version值
   */
  static extracted(value: string, parameterFileObj: object | undefined, pathParamsObj: pathParamsOpt, dependencyName?: string): string {
    const result = FileUtil.extractValue(value, parameterFileObj, pathParamsObj, undefined, dependencyName);
    return result.value;
  }

  /**
   * 拷贝文件夹
   * @param src
   * @param dest
   */
  static copyDir(src: string, dest: string): void {
    const fileEntries = fse.readdirSync(src, { withFileTypes: true });
    fse.mkdirsSync(dest);

    for (const fileEntry of fileEntries) {
      const srcPath = path.join(src, fileEntry.name);
      const destPath = path.join(dest, fileEntry.name);
      if (fileEntry.isDirectory()) {
        this.copyDir(srcPath, destPath);
      } else {
        fse.copyFileSync(srcPath, destPath);
      }
    }
  }

  /**
   * 判断目录是否为空
   *
   * @param {string} dirPath 目录路径
   * @returns {Promise<boolean>}
   */
  static async isEmptyDir(dirPath: string): Promise<boolean> {
    if (!fse.existsSync(dirPath)) {
      return true;
    }
    try {
      const files = await fse.readdir(dirPath);
      return files.length === 0;
    } catch (error) {
      this._log.debug(`failed to readdir ${dirPath}`);
      return false;
    }
  }
}

export interface ExtraParamsForHar {
  compatibleSdkVersion: number;
  compatibleSdkType: string;
  obfuscated: boolean;
  nativeComponents?: NativeComponents[];
}

export interface NativeComponents {
  name: string;
  compatibleSdkVersion: number;
  compatibleSdkType: string;
  linkLibraries: string[];
}
